The End of Boilerplate
For years, writing a class in Python just to hold data was tedious. You had to write an __init__ method, a __repr__ method to make it readable, and often an __eq__ method to compare instances. It was a lot of noise for very little signal.
Today, Python offers two powerful solutions to this problem: NamedTuple and @dataclass. Both allow you to create structured data types with type hints, but they serve fundamentally different purposes. The choice usually comes down to one question: Do you need to change the data after you create it?
Here is how to decide.
1. The Modern NamedTuple
The NamedTuple (from the typing module) is the type-hinted evolution of the older collections.namedtuple. It retains the memory efficiency of a standard tuple but enforces strict structure.
Think of a NamedTuple like a coordinate on a map or a row in a database. It is a snapshot of data that should not change.
Syntax
from typing import NamedTuple
class Card(NamedTuple):
rank: str
suit: str
# Usage
c1 = Card("A", "Spades")
print(c1.rank) # Output: AKey Traits
- Immutable: This is the defining feature. Once you create
c1, you cannot change it.c1.rank = "K"will raise anAttributeError. - It is a Tuple: Because it inherits from
tuple, it behaves like one. You can iterate over it (for x in c1), access items by index (c1[0]), and unpack it (rank, suit = c1). - Lightweight: It uses significantly less memory than a standard class because it doesn’t need a
__dict__to store attributes per instance.
2. The @dataclass
Introduced in Python 3.7, the @dataclass decorator is the standard for creating classes that primarily store state. It automatically generates the boilerplate code (__init__, __repr__, etc.) for you, but unlike a tuple, it creates a full-fledged object.
Think of a @dataclass like a configuration object or a player entity in a game. It holds data that might need to evolve.
Syntax
from dataclasses import dataclass
@dataclass
class CardData:
rank: str
suit: str
# Usage
c2 = CardData("A", "Hearts")
c2.rank = "K" # This works! The object is mutable.Key Traits
- Mutable: You can update fields freely after initialization.
- It is a Class: It behaves like a standard object. You cannot index it (
c2[0]fails) or unpack it without writing custom helper methods. - Rich Features: It supports complex inheritance, default values, and post-initialization processing (
__post_init__) much better thanNamedTuple.
Comparison: Which one to use?
Here is the breakdown of technical differences:
| Feature | NamedTuple |
@dataclass |
|---|---|---|
| Mutability | Immutable (Read-only) | Mutable (Read-write) |
| Type Hints | Yes | Yes |
| Memory Use | Low (Tiny overhead) | Normal (Class overhead) |
| Behavior | Acts like a Tuple (x, y) |
Acts like an Object obj.x |
| Best For | Coordinates, Return values | Configs, State, Entities |
The Decision Matrix
Choose NamedTuple if:
- Safety is a priority. You want to ensure the data is read-only and cannot be accidentally modified by another part of your code.
- You need tuple behavior. You want to unpack the data (
x, y = point) or pass it to functions that expect sequences. - Performance matters. You are processing millions of small objects and need to minimize memory footprint.
Choose @dataclass if:
- Data evolves. You are building something like a User profile where fields (like
last_loginorscore) change over time. - You need logic. You plan to add custom methods to the class (e.g.,
def is_valid(self):). - You need inheritance. Dataclasses handle class hierarchies more gracefully than named tuples.
The Hybrid Approach: frozen=True
What if you want the features of a @dataclass (like nice inheritance) but the safety of a NamedTuple (immutability)?
You can “freeze” a dataclass:
@dataclass(frozen=True)
class ImmutablePoint:
x: int
y: int
p = ImmutablePoint(10, 20)
# p.x = 15 <-- Raises FrozenInstanceErrorThis gives you an immutable object that still behaves like a class (no indexing/unpacking) rather than a tuple. It is often the best middle ground for modern Python development.
Thank you for reading!